package aws;

import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JToggleButton;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;

@SuppressWarnings("serial")
public class Switcher {

	private static Font LARGE_FONT = new Font("Monaco", Font.BOLD, 18);
	private static Font SMALL_FONT = new Font("Monaco", Font.BOLD, 12);
	
	private static Color TRANSPARENT = new Color(0, 0, 0, 0);
	private static Color DARK_TINT = new Color(0, 0, 0, 192);
	private static Color SELECTED_TINT = new Color(32, 128, 192, 220);
	
	private static class AWTableModel extends AbstractTableModel {
		private String[] apps;
		private int initialSelectionIndex = -1;
		
		AWTableModel(String[] apps) {
			this.apps = apps;
	        int rowCount = apps.length;
	        
	        if (rowCount == 0) {
	        	initialSelectionIndex = 0;
	        } else if (rowCount > 1)  {
	        	for (int i = (rowCount - 1); i >= 0; i--) {
					String value = apps[i];
					if (value.indexOf(" \u2756 ") != -1) {
						initialSelectionIndex = i + 1;
						if (initialSelectionIndex >= rowCount) {
							initialSelectionIndex = 0;
						}
						break;
					}
				}
	        }
		}
		
		public Object getValueAt(int rowIndex, int columnIndex) {
			return apps[rowIndex];
		}
		
		public int getRowCount() {
			return apps.length;
		}
		
		public int getColumnCount() {
			return 1;
		}
		
		public int getInitialSelectionIndex() {
			return initialSelectionIndex;
		}
		
	};
	
	private static Border awBorder = BorderFactory.createLineBorder(Color.white, 4);
	private static Border awBorderTRANSPARENT = BorderFactory.createLineBorder(TRANSPARENT, 4);
	
	private static class AWCellRenderer implements TableCellRenderer {
		private static final String SPACES = "                                                                                                    "; 
		private static JPanel borderPanel;
		private static DefaultTableCellRenderer delegate = new DefaultTableCellRenderer();
		
		private AWCellRenderer() {
			borderPanel = new JPanel(new BorderLayout());
			borderPanel.setOpaque(false);
			
			delegate = new DefaultTableCellRenderer();
			delegate.setOpaque(true);
			borderPanel.add((Component) delegate, BorderLayout.CENTER);
		}
		
		public Component getTableCellRendererComponent(JTable table,
				Object value, boolean isSelected, boolean hasFocus, int row,
				int column) {
			String text = String.valueOf(value);
			int length = text.length();
			if (length > 80) {
				text = text.substring(0, 77) + "...";
			}
			text = "  " + text + "  ";
			length = text.length();
			if (text.length() < 60) {
				text = text + SPACES.substring(0, (60 - length - 1));
			}
			borderPanel.setBorder(isSelected ? awBorder : awBorderTRANSPARENT);
			delegate.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column);
			return borderPanel;
		}		
	};
	
	private static TableCellRenderer awCellRenderer = new AWCellRenderer();
	
	private static final String SHOW = "\u229e";
	private static final String HIDE = "\u229f";
	
	public static void main(String[] args) {
		
		boolean moveResizeAvailable = new File(System.getProperty("user.home") + "/Library/Scripts/MoveResize.scpt").exists();
		final JFrame undecoratedFrame  = new JFrame();
		undecoratedFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		undecoratedFrame.setUndecorated(true);
		
		undecoratedFrame.addWindowListener(new WindowAdapter() {
			public void windowDeactivated(WindowEvent e) {
				// bail out
				cancelled(undecoratedFrame);
			}
		});
		
		JPanel contentPane =  new JPanel(new GridBagLayout());
		contentPane.setOpaque(true);
		contentPane.setBackground(DARK_TINT);
		
		final JTable awTable = new JTable() {
			
			public String getToolTipText(MouseEvent event) {
				int row = rowAtPoint(event.getPoint());
				if (row != -1) {
					return String.valueOf(getValueAt(row, 0)).trim();
				}
				return null;
			}
			
			@Override
			public void addNotify() {
				super.addNotify();
				requestFocusInWindow();
			}
		};
		awTable.setOpaque(false);
		awTable.setShowGrid(false);
		awTable.setForeground(Color.white);
		awTable.setBackground(TRANSPARENT);
		awTable.setSelectionBackground(SELECTED_TINT);
		awTable.setFont(LARGE_FONT);
		awTable.setRowHeight(LARGE_FONT.getSize() + 24);
		final AWTableModel awTableModel = new AWTableModel(args);
		awTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		awTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent e) {
				undecoratedFrame.getContentPane().repaint();
			}
		});
		
		awTable.setDefaultRenderer(Object.class, awCellRenderer);
		
		//awTable.setPreferredSize(new Dimension(300, awTableModel.getRowCount() * awTable.getRowHeight()));
		
		GridBagConstraints centerGBC;        
        centerGBC = new GridBagConstraints();
        centerGBC.gridx = 0;
        centerGBC.gridy = 0;
        centerGBC.weightx = 1.0;
        centerGBC.weighty = 1.0;
        centerGBC.fill = GridBagConstraints.HORIZONTAL;
        centerGBC.anchor = GridBagConstraints.CENTER;
        centerGBC.insets = new Insets(10,10,2,10);

		awTable.addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent e) {
				if (e.getClickCount() == 2) {
					doit(undecoratedFrame, awTable.getSelectedRow(), "A");
				}
			}
		});

        InputMap inputMap = awTable.getInputMap(JComponent.WHEN_FOCUSED);
        ActionMap actionMap = awTable.getActionMap();

		inputMap.put(KeyStroke.getKeyStroke("ESCAPE"), "escape");
		actionMap.put("escape", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				// bail out
				cancelled(undecoratedFrame);
			}
		});
		
		inputMap.put(KeyStroke.getKeyStroke("ENTER"), "switch");
		inputMap.put(KeyStroke.getKeyStroke("SPACE"), "switch");
		actionMap.put("switch", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				// do it and then
				doit(undecoratedFrame, awTable.getSelectedRow(), "A");
			}
		});
		
		inputMap.put(KeyStroke.getKeyStroke("shift Q"), "quit");
		inputMap.put(KeyStroke.getKeyStroke("Q"), "quit");
		actionMap.put("quit", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				// do it and then
				doit(undecoratedFrame, awTable.getSelectedRow(), "Q");
			}
		});
		
		inputMap.put(KeyStroke.getKeyStroke("shift H"), "hide");
		inputMap.put(KeyStroke.getKeyStroke("H"), "hide");
		actionMap.put("hide", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				// do it and then
				doit(undecoratedFrame, awTable.getSelectedRow(), "H");
			}
		});
		
		inputMap.put(KeyStroke.getKeyStroke("shift M"), "minimize");
		inputMap.put(KeyStroke.getKeyStroke("M"), "minimize");
		actionMap.put("minimize", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				// do it and then
				doit(undecoratedFrame, awTable.getSelectedRow(), "M");
			}
		});
		
		inputMap.put(KeyStroke.getKeyStroke("shift W"), "close");
		inputMap.put(KeyStroke.getKeyStroke("W"), "close");
		actionMap.put("close", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				// do it and then
				doit(undecoratedFrame, awTable.getSelectedRow(), "W");
			}
		});
		
		inputMap.put(KeyStroke.getKeyStroke("shift Z"), "zoom");
		inputMap.put(KeyStroke.getKeyStroke("Z"), "zoom");
		actionMap.put("zoom", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				// do it and then
				doit(undecoratedFrame, awTable.getSelectedRow(), "Z");
			}
		});
		
		if (moveResizeAvailable) {
			inputMap.put(KeyStroke.getKeyStroke("shift R"), "move-resize");
			inputMap.put(KeyStroke.getKeyStroke("R"), "move-resize");
			actionMap.put("move-resize", new AbstractAction() {
				public void actionPerformed(ActionEvent e) {
					// do it and then
					doit(undecoratedFrame, awTable.getSelectedRow(), "R");
				}
			});
		}
		
		inputMap.put(KeyStroke.getKeyStroke("TAB"), "next");
		inputMap.put(KeyStroke.getKeyStroke("DOWN"), "next");
		inputMap.put(KeyStroke.getKeyStroke("BACK_QUOTE"), "next");
		inputMap.put(KeyStroke.getKeyStroke("shift N"), "next");
		inputMap.put(KeyStroke.getKeyStroke("N"), "next");
		actionMap.put("next", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				// next
				int selectedRow = awTable.getSelectedRow();
				++selectedRow;
				int rows = awTable.getRowCount();
				if (selectedRow >= rows){
					selectedRow = 0;
				}
				awTable.getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
			}
		});
		
		inputMap.put(KeyStroke.getKeyStroke("shift TAB"), "previous");
		inputMap.put(KeyStroke.getKeyStroke("UP"), "previous");
		inputMap.put(KeyStroke.getKeyStroke("shift BACK_QUOTE"), "previous");
		inputMap.put(KeyStroke.getKeyStroke("shift P"), "previous");
		inputMap.put(KeyStroke.getKeyStroke("P"), "previous");
		actionMap.put("previous", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				int selectedRow = awTable.getSelectedRow();
				int rows = awTable.getRowCount();
				--selectedRow;
				if (selectedRow < 0){
					selectedRow = rows - 1;
				}
				awTable.getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
			}
		});
		
		autoResizeColWidth(awTable ,awTableModel);
		
        contentPane.add(awTable, centerGBC);
        
        final JToggleButton showHideLegend = new JToggleButton(SHOW);
        showHideLegend.setFont(SMALL_FONT);
        showHideLegend.setForeground(Color.white);
        showHideLegend.setFocusPainted(false);
        showHideLegend.setFocusable(false);
        showHideLegend.setBorderPainted(false);
        showHideLegend.setMargin(new Insets(1,1,1,1));
        showHideLegend.setToolTipText("Show legend - Click or type ?");
        centerGBC = new GridBagConstraints();
        centerGBC.gridx = 0;
        centerGBC.gridy = 1;
        centerGBC.weightx = 0;
        centerGBC.weighty = 0;
        centerGBC.fill = GridBagConstraints.NONE;
        centerGBC.anchor = GridBagConstraints.WEST;
        centerGBC.insets = new Insets(0,0,2,0);

        contentPane.add(showHideLegend, centerGBC);
        
        final JLabel legend = new JLabel("", JLabel.CENTER);
        legend.setForeground(Color.lightGray.brighter());
        
        StringBuilder legendText = new StringBuilder();
        legendText.append("<html>")
                  .append("<div style='font-family: Monaco;font-size: 10;'>")
                  .append("<table>")
                  .append("<tr><td align='right'>DOWN or n or TAB</td><td style='margin-left: 5px;'>Next</td>")
                  .append("    <td align='right' style='margin-left: 10px;'>UP or p or Shift TAB</td><td style='margin-left: 5px;'>Previous</td></tr>")
                  .append("<tr><td align='right'>DOUBL CLICK or ENTER or SPACE</td><td style='margin-left: 5px;'>Switch to Window or Application</td>")
                  .append("    <td align='right' style='margin-left: 10px;'>ESCAPE</td><td style='margin-left: 5px;'>Cancel</td></tr>")
                  .append("<tr><td align='right'>q</td><td style='margin-left: 5px;'>Quit Application</td>")
                  .append("    <td align='right' style='margin-left: 10px;'>h</td><td style='margin-left: 5px;'>Hide Application</td></tr>")
                  .append("<tr><td align='right'>w</td><td style='margin-left: 5px;'>Close Window</td>")
                  .append("    <td align='right' style='margin-left: 10px;'>m</td><td style='margin-left: 5px;'>Minimize Window</td></tr>")
                  .append(("<tr><td align='right'>z</td><td style='margin-left: 5px;'>Zoom/Unzoom Window</td>"))
        		  .append((moveResizeAvailable ? 
        				  "    <td align='right' style='margin-left: 10px;'>r</td><td style='margin-left: 5px;'>Resize/Move Window</td></tr>"
        				  :
                          "    <td align='right' style='margin-left: 10px;'>&nbsp;</td><td style='margin-left: 5px;'>&nbsp;</td></tr>"
                		  ))
                  .append("</table>")
                  .append("</div>")
                  ;
        legend.setText(legendText.toString());
        
        centerGBC = new GridBagConstraints();
        centerGBC.gridx = 0;
        centerGBC.gridy = 2;
        centerGBC.weightx = 1.0;
        centerGBC.weighty = 1.0;
        centerGBC.fill = GridBagConstraints.HORIZONTAL;
        centerGBC.anchor = GridBagConstraints.CENTER;
        centerGBC.insets = new Insets(0,10,2,10);

        contentPane.add(legend, centerGBC);
        
        undecoratedFrame.setContentPane(contentPane);
        undecoratedFrame.pack();
		
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        
        Rectangle undecoratedFrameBounds = undecoratedFrame.getBounds();
        
		int x = (screenSize.width - undecoratedFrameBounds.width)/2;
		int y = (screenSize.height - undecoratedFrameBounds.height)/2;
		final Image screenCapture = getRobot().createScreenCapture(
				new Rectangle(
						x
						,y
						,undecoratedFrameBounds.width
						,undecoratedFrameBounds.height						
				));
        
        undecoratedFrame.setContentPane(wrapInBackgroundImage(contentPane,
                new ImageIcon(screenCapture)));
        
        legend.setVisible(false);
        undecoratedFrame.pack();
        
        int initialSelectionIndex = awTableModel.getInitialSelectionIndex();
        awTable.getSelectionModel().setSelectionInterval(initialSelectionIndex, initialSelectionIndex);
        
		undecoratedFrame.setAlwaysOnTop(true);
		undecoratedFrame.setLocation(x, y);
		undecoratedFrame.setVisible(true);
		
        showHideLegend.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				Object source = e.getSource();
				if (source instanceof JToggleButton) {
					JToggleButton toggleButton = (JToggleButton) source;
					if (toggleButton.isSelected()) {
						toggleButton.setText(HIDE);
						toggleButton.setToolTipText("Hide legend - Click or type ?");					
						legend.setVisible(true);
					} else {
						toggleButton.setText(SHOW);
						toggleButton.setToolTipText("Show legend - Click or type ?");					
						legend.setVisible(false);
					}
					undecoratedFrame.pack();
					undecoratedFrame.getRootPane().repaint();
					undecoratedFrame.repaint(500);
				}
			}
		});
        
        
        inputMap.put(KeyStroke.getKeyStroke("shift SLASH"), "togglelegend");
		actionMap.put("togglelegend", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				JToggleButton toggleButton = showHideLegend;
				toggleButton.setSelected(!toggleButton.isSelected());
				if (toggleButton.isSelected()) {
					toggleButton.setToolTipText("Hide legend - Click or type ?");
					toggleButton.setText(HIDE);
					legend.setVisible(true);
				} else {
					toggleButton.setToolTipText("Show legend - Click or type ?");
					toggleButton.setText(SHOW);
					legend.setVisible(false);
				}
				undecoratedFrame.pack();
				undecoratedFrame.getRootPane().repaint();
				undecoratedFrame.repaint(500);
			}
		});
	}
	
	private static void doit(JFrame frame, int index, String arg) {
		System.out.print("" + (index+1) + ":" + arg);
		done(frame);
	}
	
	private static void cancelled(JFrame frame) {
		done(frame);
	}
	
	private static void done(JFrame frame) {
		System.out.println("");
		System.out.flush();
		frame.setVisible(false);
		System.exit(0);
	}
	
	private static Robot robot;
	
	public static Robot getRobot() {
		if (robot == null) {
			try {
				robot = new Robot();
			} catch (AWTException e) {
			}
		}
		return robot;
	}
	
	// Set up contraints so that the user supplied component and the
    // background image label overlap and resize identically
    private static final GridBagConstraints gbc;
    static {
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        gbc.fill = GridBagConstraints.BOTH;
        gbc.anchor = GridBagConstraints.NORTHWEST;
    }
    
    /**
     * Wraps a Swing JComponent in a background image. Simply invokes the overloded
     * variant with Top/Leading alignment for background image.
     *
     * @param component - to wrap in the a background image
     * @param backgroundIcon - the background image (Icon)
     * @return the wrapping JPanel
     */
    public static JPanel wrapInBackgroundImage(JComponent component,
            Icon backgroundIcon) {
        return wrapInBackgroundImage(
                component,
                backgroundIcon,
                JLabel.TOP,
                JLabel.LEADING);
    }
    
    /**
     * Wraps a Swing JComponent in a background image. The vertical and horizontal
     * alignment of background image can be specified using the alignment
     * contants from JLabel.
     *
     * @param component - to wrap in the a background image
     * @param backgroundIcon - the background image (Icon)
     * @param verticalAlignment - vertical alignment. See contants in JLabel.
     * @param horizontalAlignment - horizontal alignment. See contants in JLabel.
     * @return the wrapping JPanel
     */
    public static JPanel wrapInBackgroundImage(JComponent component,
            Icon backgroundIcon,
            int verticalAlignment,
            int horizontalAlignment) {
        
        // make the passed in swing component transparent
    	// component.setOpaque(false);
        
        // create wrapper JPanel
        JPanel backgroundPanel = new JPanel(new GridBagLayout());
        
        // add the passed in swing component first to ensure that it is in front
        backgroundPanel.add(component, gbc);
        
        // create a label to paint the background image
        JLabel backgroundImage = new JLabel(backgroundIcon);
        
        // set minimum and preferred sizes so that the size of the image
        // does not affect the layout size
        backgroundImage.setPreferredSize(new Dimension(1,1));
        backgroundImage.setMinimumSize(new Dimension(1,1));
        
        // align the image as specified.
        backgroundImage.setVerticalAlignment(verticalAlignment);
        backgroundImage.setHorizontalAlignment(horizontalAlignment);
        
        // add the background label
        backgroundPanel.add(backgroundImage, gbc);
        
        // return the wrapper
        return backgroundPanel;
    }
    
    private static JTable autoResizeColWidth(JTable table, AbstractTableModel model) {
        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        table.setModel(model);
 
        int margin = 5;
 
        for (int i = 0; i < table.getColumnCount(); i++) {
            int                     vColIndex = i;
            DefaultTableColumnModel colModel  = (DefaultTableColumnModel) table.getColumnModel();
            TableColumn             col       = colModel.getColumn(vColIndex);
            int                     width     = 0;
 
            // Get width of column header
            TableCellRenderer renderer = col.getHeaderRenderer();
 
            if (renderer == null) {
                renderer = table.getTableHeader().getDefaultRenderer();
            }
 
            Component comp = renderer.getTableCellRendererComponent(table, col.getHeaderValue(), false, false, 0, 0);
 
            width = comp.getPreferredSize().width;
 
            // Get maximum width of column data
            for (int r = 0; r < table.getRowCount(); r++) {
                renderer = table.getCellRenderer(r, vColIndex);
                comp     = renderer.getTableCellRendererComponent(table, table.getValueAt(r, vColIndex), false, false,
                        r, vColIndex);
                width = Math.max(width, comp.getPreferredSize().width);
            }
 
            // Add margin
            width += 2 * margin;
 
            // Set the width
            col.setPreferredWidth(width);
        }
 
        ((DefaultTableCellRenderer) table.getTableHeader().getDefaultRenderer()).setHorizontalAlignment(
            SwingConstants.LEFT);
 
        // table.setAutoCreateRowSorter(true);
        table.getTableHeader().setReorderingAllowed(false);
 
        return table;
    }

}
